﻿using System;
using System.Collections.Generic;
using System.Data.Services.Client;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.WindowsAzure.StorageClient;

namespace jp110311.StorageModels
{
    [CLSCompliant(false)]
    public class TableStorageIndexManager<TEntity, TIndexEntity>
        where TEntity : TableServiceEntity
        where TIndexEntity : TableServiceEntity
    {
        private readonly char[] _separatorCharArray;
        private readonly string _separatorString;
        private readonly char _indexItemSeparator;

        private readonly Func<TIndexEntity, string> _indexGetter;
        private readonly Action<TIndexEntity, string> _indexSetter;

        readonly Dictionary<Tuple<TableServiceContext, TIndexEntity>, IndexItemsList> _parsedIndex =
            new Dictionary<Tuple<TableServiceContext, TIndexEntity>, IndexItemsList>();

        private static readonly Dictionary<
            Expression<Func<TIndexEntity, string>>,
            IndexAccessorSet>
            IndexAccessorSets
                = new Dictionary<Expression<Func<TIndexEntity, string>>, IndexAccessorSet>();

        internal class IndexAccessorSet
        {
            public Func<TIndexEntity, string> Getter { get; private set; }
            public Action<TIndexEntity, string> Setter { get; private set; }

            public IndexAccessorSet(Func<TIndexEntity, string> getter, Action<TIndexEntity, string> setter)
            {
                Getter = getter;
                Setter = setter;
            }
        }

        internal class IndexItemsList
        {
            internal List<string> Items { get; set; }
            internal bool IsDirty { get; private set; }

            public void SetDirty(TableServiceContextManager contextManager, TIndexEntity indexEntity, Action<TIndexEntity, string> indexSetter, string separator)
            {
                if (IsDirty) return;
                IsDirty = true;
                contextManager.BeforeSaveChange +=
                    (sender, e) =>
                    {
                        if (!IsDirty) return;
                        indexSetter(indexEntity, string.Join(separator, Items));
                        contextManager.Context.UpdateObject(indexEntity);
                        IsDirty = false;
                    };
            }
        }

        public TableStorageIndexManager(
            Expression<Func<TIndexEntity, string>> indexProperty,
            char indexItemSeparator, string tableName)
        {
            var member = indexProperty.Body as MemberExpression;
            if (member == null) throw new ArgumentOutOfRangeException("indexProperty", "ラムダ式をメンバ参照に変換できませんでした。");

            _indexItemSeparator = indexItemSeparator;
            _tableName = tableName;
            _separatorCharArray = new[] { _indexItemSeparator };
            _separatorString = new string(_indexItemSeparator, 1);
            _indexGetter = indexProperty.Compile();

            IndexAccessorSet accesorSet;
            if (IndexAccessorSets.TryGetValue(indexProperty, out accesorSet))
            {
                _indexGetter = accesorSet.Getter;
                _indexSetter = accesorSet.Setter;
                return;
            }
            var param = Expression.Parameter(typeof(TIndexEntity), "target");
            var paramValue = Expression.Parameter(typeof(string), "value");
            Debug.Assert(member != null);
            var lambda =
            Expression.Lambda<Action<TIndexEntity, string>>(
                Expression.Assign(
                    Expression.MakeMemberAccess(
                        param,
                        member.Member
                        ),
                    paramValue
                    ),
                param,
                paramValue);
            _indexSetter = lambda.Compile();
            IndexAccessorSets[indexProperty] =
                new IndexAccessorSet(_indexGetter, _indexSetter);
        }

        private int _internalPageSize = 15;
        private readonly string _tableName;

        public int InternalPageSize
        {
            get { return _internalPageSize; }
            set { _internalPageSize = value; }
        }

        readonly TableStoragePredicateUtility<TEntity> _predicateUtility = new TableStoragePredicateUtility<TEntity>();

        public IEnumerable<TEntity> FetchItems( TableServiceContextManager contextManager, TIndexEntity indexEntity, int skipCount, int maxCount )
        {
            var itemsList = GetItemsList(contextManager, indexEntity);
            if( itemsList.Items.Count()<skipCount ) return Enumerable.Empty<TEntity>();

            var rowKeys = itemsList.Items.Skip(skipCount).Take(maxCount);

            var result = new List<TEntity>();
            int lastFetched;

            var tasks = new List<Task<IEnumerable<TEntity>>>();
            // 並列にクエリを発行して
            for( int fetched=0; fetched<maxCount; fetched += lastFetched )
            {
                int reast = maxCount - fetched;
                if( reast>InternalPageSize )
                {
                    reast = InternalPageSize;
                }

                var fetchKeys = rowKeys.Skip(fetched).Take(reast);
                if (fetchKeys.Count() != 0)
                {
                    var task = new Task<IEnumerable<TEntity>>(
                        keys =>
                        {
                            var query = contextManager.Context.CreateQuery<TEntity>(_tableName);
                            var lambda = _predicateUtility.BuildLambda(indexEntity.PartitionKey, (IEnumerable<string>)keys);
                            return query.Where(lambda).AsEnumerable();
                        }, fetchKeys);  
                    
                    task.Start();
                    tasks.Add(task);
                }
                lastFetched = reast;
            }
            // 結果をまとめる
            foreach (var task in tasks)
            {
                var dic= task.Result.ToDictionary(item => item.RowKey);
                var keys = task.AsyncState as IEnumerable<string>;
                Debug.Assert(keys!=null);

                result.AddRange( keys.Select(k=> dic[k]) );
            }
            
            return result;
        }

        public void AddItem(TableServiceContextManager contextManager, TIndexEntity indexEntity, string item)
        {
            IndexItemsList itemsList = GetItemsList(contextManager, indexEntity);
            if( itemsList.Items.Contains(item )) return;

            itemsList.Items.Add(item);
            itemsList.SetDirty(contextManager, indexEntity, _indexSetter, _separatorString);
            return;
        }

        public void RemoveItem( TableServiceContextManager contextManager, TIndexEntity indexEntity, string item)
        {
            IndexItemsList itemsList = GetItemsList(contextManager, indexEntity);
            if (itemsList.Items.Remove(item))
            {
                itemsList.SetDirty(contextManager, indexEntity, _indexSetter, _separatorString);
            }
            return;            
        }

        private IndexItemsList GetItemsList(TableServiceContextManager contextManager, TIndexEntity indexEntity)
        {
            IndexItemsList itemsList;
            var keyTuple = new Tuple<TableServiceContext, TIndexEntity>(contextManager.Context, indexEntity);
            if (!_parsedIndex.TryGetValue(keyTuple, out itemsList))
            {
                var index = _indexGetter(indexEntity);
                var items = index.Split(_separatorCharArray, StringSplitOptions.RemoveEmptyEntries).ToList();
                itemsList = new IndexItemsList
                            {
                                Items = items
                            };
                _parsedIndex.Add(keyTuple, itemsList);
            }
            return itemsList;
        }
    }

    public class TableServiceContextManager
    {
        public TableServiceContext Context { get; private set; }

        public TableServiceContextManager( TableServiceContext context )
        {
            Context = context;
        }

        public event EventHandler BeforeSaveChange;

        public void DoSaveChanges()
        {
            DoSaveChanges( SaveChangesOptions.None );
        }

        public void DoSaveChanges( SaveChangesOptions saveChangesOptions )
        {
            if( BeforeSaveChange!=null )
            {
                BeforeSaveChange(this, EventArgs.Empty);
            }
            Context.SaveChanges(saveChangesOptions);
        }
    }
}
